Skip to content

fix(store): handle AllEntries pagination for genesis block storage maps#1816

Merged
mmagician merged 9 commits into0xMiden:release/v0.14.0-alphafrom
mmagician:mmagician-claude/fix-allentries-genesis
Mar 23, 2026
Merged

fix(store): handle AllEntries pagination for genesis block storage maps#1816
mmagician merged 9 commits into0xMiden:release/v0.14.0-alphafrom
mmagician:mmagician-claude/fix-allentries-genesis

Conversation

@mmagician
Copy link
Contributor

Summary

  • Fixes a bug where GetAccount RPC with AllEntries(true) returns an internal error for storage maps with entries exceeding the pagination limit when all entries are in genesis block 0
  • Root cause: select_account_storage_map_values_paged computes last_block_num.saturating_sub(1) = -1 (i64) when all entries share the same block, which fails BlockNumber::from_raw_sql
  • Fix: when take_while yields empty results (single-block overflow), return block_range.start() to signal no pagination progress, triggering limit_exceeded in the caller

Discovered via 0xMiden/miden-client#1926

Test plan

  • Added test select_storage_map_sync_values_all_entries_in_genesis_block that reproduces the bug (fails without fix, passes with fix)
  • Full miden-node-store test suite passes (121 tests)

🤖 Generated with Claude Code

mmagician and others added 3 commits March 20, 2026 10:39
chore: bring `v0.14.0 alpha` changes to `next`
…sis block

When all storage map entries exceed the pagination limit and reside in
a single block (e.g. genesis block 0), `take_while` produces empty
results since all rows share the same block_num. Previously this led
to `last_block_num.saturating_sub(1)` = -1 (i64) which failed
BlockNumber::from_raw_sql, returning an internal error to the client.

Now when take_while yields no values (all entries in one block), we
return block_range.start() as last_block_included with empty values.
The caller (reconstruct_storage_map_from_db) interprets this as no
pagination progress and returns limit_exceeded, which is the correct
response for maps exceeding the entry limit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@igamigo
Copy link
Collaborator

igamigo commented Mar 21, 2026

Haven't seen the context of the error to see if the fix makes sense, but It's worth noting that the client test was (a bit hackily) adding more data to the account than would realistically be allowed in a single block (to trigger the "too many entries" account data code paths).

Comment on lines +730 to +737
if values.is_empty() {
// All entries are in the same block and exceed the limit.
// Return the range start to signal no progress was made,
// which the caller interprets as limit_exceeded.
(*block_range.start(), values)
} else {
(BlockNumber::from_raw_sql(last_block_num.saturating_sub(1))?, values)
}
Copy link
Collaborator

@Mirko-von-Leipzig Mirko-von-Leipzig Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the original code had the correct idea, but didn't realise that the sql value was signed integer.

This has come up a few time, might be worth adding saturating_sub and friends to block number @PhilippGackstatter?

Suggested change
if values.is_empty() {
// All entries are in the same block and exceed the limit.
// Return the range start to signal no progress was made,
// which the caller interprets as limit_exceeded.
(*block_range.start(), values)
} else {
(BlockNumber::from_raw_sql(last_block_num.saturating_sub(1))?, values)
}
(BlockNumber::from_raw_sql(last_block_num)?.parent().unwrap_or_default(), values)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest commit e0a7ba8 takes a different approach now and derives last_block_included from the last kept row:

let kept: Vec<_> = raw.into_iter()
    .take_while(|(bn, ..)| *bn != last_block_num)
    .collect();

let last_block_included = match kept.last() {
    Some(&(bn, ..)) => BlockNumber::from_raw_sql(bn)?,
    None => *block_range.start(),
};

The block_num - 1 approach assumed the previous block number is meaningful - but after take_while, we already have the actual rows we're keeping, so we can just read the block number directly and avoid any arithmetic on block numbers altogether.


(independently adding a saturating_sub method to BlockNumber can still be useful in general).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't the latest approach require two allocations vs. just one allocation with the original approach?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, good catch, this ends up with one allocation for raw values and a second for StorageMapValue. But they can be collapsed into a single allocation - fixed in a6016ed

claude and others added 2 commits March 22, 2026 14:24
…hmetic

Replace `last_block_num.saturating_sub(1)` with reading the block
number from the last kept row after take_while. This is correct for
all cases:
- Multiple blocks: uses the actual last block we're returning
- Single block overflow: returns block_range.start() (no progress)
- No assumption about contiguous block numbers

Also adds tests for single non-genesis block overflow and multi-block
pagination to verify correctness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

fix: address clippy lints and rename variable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mmagician mmagician force-pushed the mmagician-claude/fix-allentries-genesis branch from 3507558 to 3d13100 Compare March 22, 2026 14:24
@mmagician
Copy link
Contributor Author

t's worth noting that the client test was (a bit hackily) adding more data to the account than would realistically be allowed in a single block (to trigger the "too many entries" account data code paths).

Ack. I think this only would happen for a genesis block anyway? But if my understanding of the problem is correct, the latest approach in e0a7ba8 should handle all the previous scenarios correctly + the genesis block.


I tested this branch on as a dependency and no fixes are needed on the client - though independently we might want to revisit the logic there and avoid a fetch with AllEntries(true) altogether, since it's already taken care of by build_storage_slots

@mmagician
Copy link
Contributor Author

Of related interest: if we decide to take the approach in this PR, we might also want to apply the same pagination logic to assets etc.

claude and others added 2 commits March 23, 2026 07:27
Map raw rows to StorageMapValue in one pass, then read
last_block_included from the last element's block_num field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mmagician and others added 2 commits March 23, 2026 10:48
Co-authored-by: Mirko <48352201+Mirko-von-Leipzig@users.noreply.github.com>
@mmagician mmagician marked this pull request as ready for review March 23, 2026 10:27
@Mirko-von-Leipzig
Copy link
Collaborator

#1817 bot seems to have found several replicas in other methods :)

@mmagician mmagician changed the base branch from next to release/v0.14.0-alpha March 23, 2026 15:39
@mmagician
Copy link
Contributor Author

Changed base to release/v0.14.0-alpha

@mmagician mmagician merged commit bce00da into 0xMiden:release/v0.14.0-alpha Mar 23, 2026
15 checks passed
@mmagician mmagician deleted the mmagician-claude/fix-allentries-genesis branch March 23, 2026 15:46
mmagician pushed a commit to mmagician/miden-node that referenced this pull request Mar 23, 2026
…tion

Replace `last_block_num.saturating_sub(1)` with reading block_num from
the last kept `AccountVaultValue`. Same approach as the storage map fix
in 0xMiden#1816.

No unit test added because `select_account_vault_assets` uses a
hardcoded `MAX_ROWS` (~61k) derived from `MAX_RESPONSE_PAYLOAD_BYTES`,
making it impractical to trigger the overflow in a test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mmagician pushed a commit to mmagician/miden-node that referenced this pull request Mar 23, 2026
…pagination

Replace `last_block_num.saturating_sub(1)` with reading block_num from
the last kept `TransactionRecordRaw`. Same approach as the storage map
fix in 0xMiden#1816.

This is unlikely to be triggered in practice (genesis blocks don't have
transactions, and a single non-genesis block is unlikely to exceed the
4MB size-based limit), but we fix it for consistency and safety.

No unit test added because the function uses `MAX_RESPONSE_PAYLOAD_BYTES`
(4MB) as the size limit, making it impractical to trigger in a test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants